package lsc;

import java.awt.Color;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;

import robocode.AdvancedRobot;
import robocode.BulletHitEvent;
import robocode.BulletMissedEvent;
import robocode.HitByBulletEvent;
import robocode.RobotDeathEvent;
import robocode.ScannedRobotEvent;
import robocode.util.Utils;

public class TopGun extends AdvancedRobot {
	final double PI = Math.PI;		//just a constant
	public static int BINS = 47;
    public static double _surfStats[] = new double[BINS];
    public Point2D.Double _myLocation;     // our bot's location
    public Point2D.Double _enemyLocation;  // enemy bot's location

    public ArrayList _enemyWaves;
    public ArrayList _surfDirections;
    public ArrayList _surfAbsBearings;

    public static double _oppEnergy = 100.0;
    /** This is a rectangle that represents an 800x600 battle field,
     * used for a simple, iterative WallSmoothing method (by PEZ).
     * If you're not familiar with WallSmoothing, the wall stick indicates
     * the amount of space we try to always have on either end of the tank
     * (extending straight out the front or back) before touching a wall.
    */
    public static Rectangle2D.Double _fieldRect
    	= new java.awt.geom.Rectangle2D.Double(18, 18, 764, 564);
    public static double WALL_STICK = 160;
    
    private static int missedCount = 0;
    private static double angleOffset = 0;
    private static double MaxAngleOffset = 0.7;
    private static double lateralDirection = 1;
    private static enum BattleMode { SINGLE, MELEE }
    private static BattleMode battleMode;
    
    // for Melee Robot
    Hashtable targets;				//all enemies are stored in the hashtable
	Enemy target;					//our current enemy
	int direction = 1;				//direction we are heading... 1 = forward, -1 = backwards
	double firePower;				//the power of the shot we will be using
	double midpointstrength = 0;	//The strength of the gravity point in the middle of the field
	int midpointcount = 0;			//Number of turns since that strength was changed.
     
	public void run() {
		setAdjustGunForRobotTurn(true);
        setAdjustRadarForGunTurn(true);
        
        while(true) {
        	if( getOthers() > 1 ) {
            	setColors(Color.red,Color.blue,Color.green);
            	battleMode = BattleMode.MELEE;
            	targets = new Hashtable();
        		target = new Enemy();
        		target.distance = 100000;	//initialise the distance so that we can select a target
        		while(getOthers() > 1) {
        			antiGravMove();				//Move the bot
        			doFirePower();				//select the fire power to use
        			doScanner();				//Oscillate the scanner over the bot
        			doGun();
        			out.println(target.distance);	//move the gun to predict where the enemy will be
        			fire(firePower);
        			execute();						//execute all commands
        		}
            }
            else {
            	setColors(Color.RED, Color.BLACK, Color.YELLOW);
            	battleMode = BattleMode.SINGLE;
            	_enemyWaves = new ArrayList();
                _surfDirections = new ArrayList();
                _surfAbsBearings = new ArrayList();
                while(true) {
        			turnRadarRightRadians(Double.POSITIVE_INFINITY);
        		}
            }
        }
	}
	
	public void onScannedRobot(ScannedRobotEvent e) {
		if( battleMode == BattleMode.MELEE ) {
			Enemy en;
			if (targets.containsKey(e.getName())) {
				en = (Enemy)targets.get(e.getName());
			} else {
				en = new Enemy();
				targets.put(e.getName(),en);
			}
			//the next line gets the absolute bearing to the point where the bot is
			double absbearing_rad = (getHeadingRadians()+e.getBearingRadians())%(2*PI);
			
			//this section sets all the information about our target
			en.name = e.getName();
			double h = normaliseBearing(e.getHeadingRadians() - en.heading);
			h = h/(getTime() - en.ctime);
			en.changehead = h;
			en.x = getX()+Math.sin(absbearing_rad)*e.getDistance(); //works out the x coordinate of where the target is
			en.y = getY()+Math.cos(absbearing_rad)*e.getDistance(); //works out the y coordinate of where the target is
			en.bearing = e.getBearingRadians();
			en.heading = e.getHeadingRadians();
			en.ctime = getTime();				//game time at which this scan was produced
			en.speed = e.getVelocity();
			en.distance = e.getDistance();	
			en.live = true;
			if ((en.distance < target.distance)||(target.live == false)) {
				target = en;
			}
		}
		else if( battleMode == BattleMode.SINGLE ) {
			_myLocation = new Point2D.Double(getX(), getY());

	        double lateralVelocity = getVelocity()*Math.sin(e.getBearingRadians());
	        double absBearing = e.getBearingRadians() + getHeadingRadians();

        	setTurnRadarRightRadians(Utils.normalRelativeAngle(absBearing
                    - getRadarHeadingRadians()) * 2);
	        
	        _surfDirections.add(0,
	            new Integer((lateralVelocity >= 0) ? 1 : -1));
	        _surfAbsBearings.add(0, new Double(absBearing + Math.PI));


	        double bulletPower = _oppEnergy - e.getEnergy();
	        if (bulletPower < 3.01 && bulletPower > 0.09
	            && _surfDirections.size() > 2) {
	            EnemyWave ew = new EnemyWave();
	            ew.fireTime = getTime() - 1;
	            ew.bulletVelocity = bulletVelocity(bulletPower);
	            ew.distanceTraveled = bulletVelocity(bulletPower);
	            ew.direction = ((Integer)_surfDirections.get(2)).intValue();
	            ew.directAngle = ((Double)_surfAbsBearings.get(2)).doubleValue();
	            ew.fireLocation = (Point2D.Double)_enemyLocation.clone(); // last tick

	            _enemyWaves.add(ew);
	        }

	        _oppEnergy = e.getEnergy();

	        // update after EnemyWave detection, because that needs the previous
	        // enemy location as the source of the wave
	        _enemyLocation = project(_myLocation, absBearing, e.getDistance());

	        updateWaves();
	        doSurfing();

	        // gun code would go here...
	        lateralDirection = sign(e.getVelocity() * Math.sin(e.getHeadingRadians() - absBearing));
	        if( e.getVelocity() == 0 ) {
	        	angleOffset = 0;
	        }
	        setTurnGunRightRadians(Utils.normalRelativeAngle(absBearing - getGunHeadingRadians() + angleOffset));
	        
	        if( _oppEnergy < 1 || e.getDistance() < 150 )
	        	fire(3);
	        else if( _oppEnergy < getEnergy() )
	        	fire(2);
	        else
	        	fire(1);
		}
	}
	
	@Override
	public void onRobotDeath(RobotDeathEvent e) {
		if( battleMode == BattleMode.MELEE ) {
			Enemy en = (Enemy)targets.get(e.getName());
			en.live = false;
		}
	}
	
	@Override
	public void onBulletMissed(BulletMissedEvent e) {
		if( battleMode == BattleMode.SINGLE ) {
			missedCount++;
			if( missedCount > 2 && angleOffset < MaxAngleOffset ) {
				angleOffset += (0.1 * lateralDirection);
				missedCount = 0;
			}
		}
	}
	
	@Override
	public void onBulletHit(BulletHitEvent event) {
		missedCount = 0;
	}
	
	void doFirePower() {
		firePower = 400/target.distance;//selects a bullet power based on our distance away from the target
		if (firePower > 3) {
			firePower = 3;
		}
	}
	
	void antiGravMove() {
   		double xforce = 0;
	    double yforce = 0;
	    double force;
	    double ang;
	    GravPoint p;
		Enemy en;
    	Enumeration e = targets.elements();
	    //cycle through all the enemies.  If they are alive, they are repulsive.  Calculate the force on us
		while (e.hasMoreElements()) {
    	    en = (Enemy)e.nextElement();
			if (en.live) {
				p = new GravPoint(en.x,en.y, -1000);
		        force = p.power/Math.pow(getRange(getX(),getY(),p.x,p.y),2);
		        //Find the bearing from the point to us
		        ang = normaliseBearing(Math.PI/2 - Math.atan2(getY() - p.y, getX() - p.x)); 
		        //Add the components of this force to the total force in their respective directions
		        xforce += Math.sin(ang) * force;
		        yforce += Math.cos(ang) * force;
			}
	    }
	    
		/**The next section adds a middle point with a random (positive or negative) strength.
		The strength changes every 5 turns, and goes between -1000 and 1000.  This gives a better
		overall movement.**/
		midpointcount++;
		if (midpointcount > 5) {
			midpointcount = 0;
			midpointstrength = (Math.random() * 2000) - 1000;
		}
		p = new GravPoint(getBattleFieldWidth()/2, getBattleFieldHeight()/2, midpointstrength);
		force = p.power/Math.pow(getRange(getX(),getY(),p.x,p.y),1.5);
	    ang = normaliseBearing(Math.PI/2 - Math.atan2(getY() - p.y, getX() - p.x)); 
	    xforce += Math.sin(ang) * force;
	    yforce += Math.cos(ang) * force;
	   
	    /**The following four lines add wall avoidance.  They will only affect us if the bot is close 
	    to the walls due to the force from the walls decreasing at a power 3.**/
	    xforce += 5000/Math.pow(getRange(getX(), getY(), getBattleFieldWidth(), getY()), 3);
	    xforce -= 5000/Math.pow(getRange(getX(), getY(), 0, getY()), 3);
	    yforce += 5000/Math.pow(getRange(getX(), getY(), getX(), getBattleFieldHeight()), 3);
	    yforce -= 5000/Math.pow(getRange(getX(), getY(), getX(), 0), 3);
	    
	    //Move in the direction of our resolved force.
	    goTo(getX()-xforce,getY()-yforce);
	}
	
	/**Move towards an x and y coordinate**/
	void goTo(double x, double y) {
	    double dist = 20; 
	    double angle = Math.toDegrees(absbearing(getX(),getY(),x,y));
	    double r = turnTo(angle);
	    setAhead(dist * r);
	}
	
	//if a bearing is not within the -pi to pi range, alters it to provide the shortest angle
	double normaliseBearing(double ang) {
		if (ang > PI)
			ang -= 2*PI;
		if (ang < -PI)
			ang += 2*PI;
		return ang;
	}
	
	//if a heading is not within the 0 to 2pi range, alters it to provide the shortest angle
	double normaliseHeading(double ang) {
		if (ang > 2*PI)
			ang -= 2*PI;
		if (ang < 0)
			ang += 2*PI;
		return ang;
	}
	
	//gets the absolute bearing between to x,y coordinates
	public double absbearing( double x1,double y1, double x2,double y2 )
	{
		double xo = x2-x1;
		double yo = y2-y1;
		double h = getRange( x1,y1, x2,y2 );
		if( xo > 0 && yo > 0 )
		{
			return Math.asin( xo / h );
		}
		if( xo > 0 && yo < 0 )
		{
			return Math.PI - Math.asin( xo / h );
		}
		if( xo < 0 && yo < 0 )
		{
			return Math.PI + Math.asin( -xo / h );
		}
		if( xo < 0 && yo > 0 )
		{
			return 2.0*Math.PI - Math.asin( -xo / h );
		}
		return 0;
	}
	
	/**Turns the shortest angle possible to come to a heading, then returns the direction the
	the bot needs to move in.**/
	int turnTo(double angle) {
	    double ang;
    	int dir;
	    ang = normaliseBearing(getHeading() - angle);
	    if (ang > 90) {
	        ang -= 180;
	        dir = -1;
	    }
	    else if (ang < -90) {
	        ang += 180;
	        dir = -1;
	    }
	    else {
	        dir = 1;
	    }
	    setTurnLeft(ang);
	    return dir;
	}
	
	/**keep the scanner turning**/
	void doScanner() {
		setTurnRadarLeftRadians(2*PI);
	}
	
	/**Move the gun to the predicted next bearing of the enemy**/
	void doGun() {
		long time = getTime() + (int)Math.round((getRange(getX(),getY(),target.x,target.y)/(20-(3*firePower))));
		Point2D.Double p = target.guessPosition(time);
		
		//offsets the gun by the angle to the next shot based on linear targeting provided by the enemy class
		double gunOffset = getGunHeadingRadians() - (Math.PI/2 - Math.atan2(p.y - getY(), p.x - getX()));
		setTurnGunLeftRadians(normaliseBearing(gunOffset));
	}
	
	int sign(double v) {
		return v < 0 ? -1 : 1;
	}
	
	/**Returns the distance between two points**/
	double getRange(double x1,double y1, double x2,double y2) {
	    double x = x2-x1;
	    double y = y2-y1;
	    double range = Math.sqrt(x*x + y*y);
	    return range;	
	}
	
	class GravPoint {
	    public double x,y,power;
	    public GravPoint(double pX,double pY,double pPower) {
	        x = pX;
	        y = pY;
	        power = pPower;
	    }
	}
	
	public void updateWaves() {
        for (int x = 0; x < _enemyWaves.size(); x++) {
            EnemyWave ew = (EnemyWave)_enemyWaves.get(x);

            ew.distanceTraveled = (getTime() - ew.fireTime) * ew.bulletVelocity;
            if (ew.distanceTraveled >
                _myLocation.distance(ew.fireLocation) + 50) {
                _enemyWaves.remove(x);
                x--;
            }
        }
    }
	
	public EnemyWave getClosestSurfableWave() {
        double closestDistance = 50000; // I juse use some very big number here
        EnemyWave surfWave = null;

        for (int x = 0; x < _enemyWaves.size(); x++) {
            EnemyWave ew = (EnemyWave)_enemyWaves.get(x);
            double distance = _myLocation.distance(ew.fireLocation)
                - ew.distanceTraveled;

            if (distance > ew.bulletVelocity && distance < closestDistance) {
                surfWave = ew;
                closestDistance = distance;
            }
        }

        return surfWave;
    }
	
	// Given the EnemyWave that the bullet was on, and the point where we
    // were hit, calculate the index into our stat array for that factor.
    public static int getFactorIndex(EnemyWave ew, Point2D.Double targetLocation) {
        double offsetAngle = (absoluteBearing(ew.fireLocation, targetLocation)
            - ew.directAngle);
        double factor = Utils.normalRelativeAngle(offsetAngle)
            / maxEscapeAngle(ew.bulletVelocity) * ew.direction;

        return (int)limit(0,
            (factor * ((BINS - 1) / 2)) + ((BINS - 1) / 2),
            BINS - 1);
    }
    
 // Given the EnemyWave that the bullet was on, and the point where we
    // were hit, update our stat array to reflect the danger in that area.
    public void logHit(EnemyWave ew, Point2D.Double targetLocation) {
        int index = getFactorIndex(ew, targetLocation);

        for (int x = 0; x < BINS; x++) {
            // for the spot bin that we were hit on, add 1;
            // for the bins next to it, add 1 / 2;
            // the next one, add 1 / 5; and so on...
            _surfStats[x] += 1.0 / (Math.pow(index - x, 2) + 1);
        }
    }
    
    public void onHitByBullet(HitByBulletEvent e) {
        // If the _enemyWaves collection is empty, we must have missed the
        // detection of this wave somehow.
        if (!_enemyWaves.isEmpty()) {
            Point2D.Double hitBulletLocation = new Point2D.Double(
                e.getBullet().getX(), e.getBullet().getY());
            EnemyWave hitWave = null;

            // look through the EnemyWaves, and find one that could've hit us.
            for (int x = 0; x < _enemyWaves.size(); x++) {
                EnemyWave ew = (EnemyWave)_enemyWaves.get(x);

                if (Math.abs(ew.distanceTraveled -
                    _myLocation.distance(ew.fireLocation)) < 50
                    && Math.round(bulletVelocity(e.getBullet().getPower()) * 10)
                       == Math.round(ew.bulletVelocity * 10)) {
                    hitWave = ew;
                    break;
                }
            }

            if (hitWave != null) {
                logHit(hitWave, hitBulletLocation);

                // We can remove this wave now, of course.
                _enemyWaves.remove(_enemyWaves.lastIndexOf(hitWave));
            }
        }
    }
    
    public Point2D.Double predictPosition(EnemyWave surfWave, int direction) {
        Point2D.Double predictedPosition = (Point2D.Double)_myLocation.clone();
        double predictedVelocity = getVelocity();
        double predictedHeading = getHeadingRadians();
        double maxTurning, moveAngle, moveDir;

        int counter = 0; // number of ticks in the future
        boolean intercepted = false;

        do {    // the rest of these code comments are rozu's
            moveAngle =
                wallSmoothing(predictedPosition, absoluteBearing(surfWave.fireLocation,
                predictedPosition) + (direction * (Math.PI/2)), direction)
                - predictedHeading;
            moveDir = 1;

            if(Math.cos(moveAngle) < 0) {
                moveAngle += Math.PI;
                moveDir = -1;
            }

            moveAngle = Utils.normalRelativeAngle(moveAngle);

            // maxTurning is built in like this, you can't turn more then this in one tick
            maxTurning = Math.PI/720d*(40d - 3d*Math.abs(predictedVelocity));
            predictedHeading = Utils.normalRelativeAngle(predictedHeading
                + limit(-maxTurning, moveAngle, maxTurning));

            // this one is nice ;). if predictedVelocity and moveDir have
            // different signs you want to breack down
            // otherwise you want to accelerate (look at the factor "2")
            predictedVelocity +=
                (predictedVelocity * moveDir < 0 ? 2*moveDir : moveDir);
            predictedVelocity = limit(-8, predictedVelocity, 8);

            // calculate the new predicted position
            predictedPosition = project(predictedPosition, predictedHeading,
                predictedVelocity);

            counter++;

            if (predictedPosition.distance(surfWave.fireLocation) <
                surfWave.distanceTraveled + (counter * surfWave.bulletVelocity)
                + surfWave.bulletVelocity) {
                intercepted = true;
            }
        } while(!intercepted && counter < 500);

        return predictedPosition;
    }
    
    public double checkDanger(EnemyWave surfWave, int direction) {
        int index = getFactorIndex(surfWave,
            predictPosition(surfWave, direction));

        return _surfStats[index];
    }

    public void doSurfing() {
        EnemyWave surfWave = getClosestSurfableWave();

        if (surfWave == null) { return; }

        double dangerLeft = checkDanger(surfWave, -1);
        double dangerRight = checkDanger(surfWave, 1);

        double goAngle = absoluteBearing(surfWave.fireLocation, _myLocation);
        if (dangerLeft < dangerRight) {
            goAngle = wallSmoothing(_myLocation, goAngle - (Math.PI/2), -1);
        } else {
            goAngle = wallSmoothing(_myLocation, goAngle + (Math.PI/2), 1);
        }

        setBackAsFront(this, goAngle);
    }
	
	class EnemyWave {
        Point2D.Double fireLocation;
        long fireTime;
        double bulletVelocity, directAngle, distanceTraveled;
        int direction;

        public EnemyWave() { }
    }

    public double wallSmoothing(Point2D.Double botLocation, double angle, int orientation) {
        while (!_fieldRect.contains(project(botLocation, angle, WALL_STICK))) {
            angle += orientation*0.05;
        }
        return angle;
    }

    public static Point2D.Double project(Point2D.Double sourceLocation,
        double angle, double length) {
        return new Point2D.Double(sourceLocation.x + Math.sin(angle) * length,
            sourceLocation.y + Math.cos(angle) * length);
    }

    public static double absoluteBearing(Point2D.Double source, Point2D.Double target) {
        return Math.atan2(target.x - source.x, target.y - source.y);
    }

    public static double limit(double min, double value, double max) {
        return Math.max(min, Math.min(value, max));
    }

    public static double bulletVelocity(double power) {
        return (20.0 - (3.0*power));
    }

    public static double maxEscapeAngle(double velocity) {
        return Math.asin(8.0/velocity);
    }

    public static void setBackAsFront(AdvancedRobot robot, double goAngle) {
        double angle =
            Utils.normalRelativeAngle(goAngle - robot.getHeadingRadians());
        if (Math.abs(angle) > (Math.PI/2)) {
            if (angle < 0) {
                robot.setTurnRightRadians(Math.PI + angle);
            } else {
                robot.setTurnLeftRadians(Math.PI - angle);
            }
            robot.setBack(100);
        } else {
            if (angle < 0) {
                robot.setTurnLeftRadians(-1*angle);
           } else {
                robot.setTurnRightRadians(angle);
           }
            robot.setAhead(100);
        }
    }
    
    class Enemy {
    	/*
    	 * ok, we should really be using accessors and mutators here,
    	 * (i.e getName() and setName()) but life's too short.
    	 */
    	String name;
    	public double bearing,heading,speed,x,y,distance,changehead;
    	public long ctime; 		//game time that the scan was produced
    	public boolean live; 	//is the enemy alive?
    	public Point2D.Double guessPosition(long when) {
    		double diff = when - ctime;
    		double newY = y + Math.cos(heading) * speed * diff;
    		double newX = x + Math.sin(heading) * speed * diff;
    		
    		return new Point2D.Double(newX, newY);
    	}
    }
}